前言
阅读此教程需要一定的C++和JUCE基础,如果没有可以翻阅我的其他文章和视频教程。
核心内容为UI设计,Slider样式的高阶设计技巧。
Github原地址:https://github.com/szkkng/ModernDial
我的Bilibili频道:香芋派Taro
我的公众号:香芋派的烘焙坊
我的音频技术交流群:1136403177
我的个人微信:JazzyTaroPie
准备
打开Projucer并创建一个新项目。因为这篇教程只涉及到UI部分,所以我们选择”GUI”模版即可。
确保这些文件都在Source目录下:
至此所有的准备都已完成,让我们开始吧!
旋钮
在这个章节中,我们会设计这个旋钮的基础部分。
自定义Slider
首先,让我们准备一个从Slider class继承的Dial class,同时override一些函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| #pragma once
#include <JuceHeader.h>
class Dial : public juce::Slider { public: Dial(); ~Dial(); void mouseDown (const juce::MouseEvent& event) override; void mouseUp (const juce::MouseEvent& event) override; private: JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Dial) }; 在下方的构造函数中,许多函数被调用了,但我只会解释其中的一部分。 Dial::Dial() { setSliderStyle (juce::Slider::SliderStyle::RotaryVerticalDrag); setTextBoxStyle (juce::Slider::TextBoxBelow, true, 80, 20); setRotaryParameters (juce::MathConstants<float>::pi * 1.25f, juce::MathConstants<float>::pi * 2.75f, true); setVelocityBasedMode (true); setVelocityModeParameters (0.5, 1, 0.09, false); setRange (0.0, 100.0, 0.01); setValue (50.0); setDoubleClickReturnValue (true, 50.0); setTextValueSuffix (" %"); onValueChange = [&]() { if (getValue() < 10) setNumDecimalPlacesToDisplay (2); else if (10 <= getValue() && getValue() < 100) setNumDecimalPlacesToDisplay (1); else setNumDecimalPlacesToDisplay (0); }; }
Dial::~Dial() { }
|
如果你对其中setRotaryParameters()这个函数比较疑惑的话,可以点击链接查看以下这篇文章,十分有用。
https://theaudioprogrammer.com/customizing-audio-plug-in-interfaces-with-juce-pt-2-creating-an-ableton-style-dial/
setVelocityBasedMode()
如果 setVelocityBasedMode() 被设置为true,不止是鼠标在被拖拽时会消失,同时还会根据鼠标的移动速度来更改值变化的速度,更加符合直觉。为了能更清晰地看清两者的区别,这里附上关闭和打开的对比图片。
False
True
由于其中有很多感知元素,这里建议自己尝试去修改ture和false来体验一下二者区别。
onValueChange
其中还有一个lambda表达式叫做 onValueChange ,当slider的值变化的时候会被执行。这个函数确保了slider在任何值时其数值都会被正常显示,具体差别如下:
Before
After
正如你看到的那样,因为数字的宽度并没有发生太大的改变,所以他看上去很美观。
mouseDown() mouseUp()
接下来,我会解释 mouseDown() 和 mouseUp() 两个函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| void Dial::mouseDown (const juce::MouseEvent& event) { juce::Slider::mouseDown (event);
setMouseCursor (juce::MouseCursor::NoCursor); }
void Dial::mouseUp (const juce::MouseEvent& event) { juce::Slider::mouseUp (event);
juce::Desktop::getInstance().getMainMouseSource().setScreenPosition (event.source.getLastMouseDownPosition()); setMouseCursor (juce::MouseCursor::NormalCursor); }
|
你可能会好奇为什么我还要再设计一个处理来在velocity模式被打开时隐藏鼠标,因为我想让鼠标在点击的瞬间被隐藏。如果仅仅只打开这个模式,那么在你的鼠标点击时光标不会被隐藏,而是在你开始拖动时才会被隐藏。
mouseUp() 中的函数确保了当鼠标释放时,光标将会回到点击瞬间时的位置。这可能看上去没有太大的差别,但这个小细节就提升了用户的使用体验。
创建旋钮对象
让我们引入头文件并开始准备三个旋钮对象。首先我们先定义他们的颜色。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| #pragma once
#include <JuceHeader.h> #include "Dial.h"
class MainComponent : public juce::Component { public: MainComponent(); ~MainComponent();
void paint (juce::Graphics&) override; void resized() override;
private: Dial blueDial, yellowDial, greenDial; juce::Colour blue = juce::Colour::fromFloatRGBA (0.43f, 0.83f, 1.0f, 1.0f); juce::Colour green = juce::Colour::fromFloatRGBA (0.34f, 0.74f, 0.66f, 1.0f); juce::Colour yellow = juce::Colour::fromFloatRGBA (1.0f, 0.71f, 0.2f, 1.0f); juce::Colour black = juce::Colour::fromFloatRGBA (0.08f, 0.08f, 0.08f, 1.0f);
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainComponent) };
|
定义的部分如下,因为并不难,所以我会跳过这些内容的解释。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| #include "MainComponent.h"
MainComponent::MainComponent() { setSize (600, 400); blueDial.setColour (juce::Slider::rotarySliderFillColourId, blue); greenDial.setColour (juce::Slider::rotarySliderFillColourId, green); yellowDial.setColour (juce::Slider::rotarySliderFillColourId, yellow); addAndMakeVisible (blueDial); addAndMakeVisible (greenDial); addAndMakeVisible (yellowDial); }
MainComponent::~MainComponent() { }
void MainComponent::paint (juce::Graphics& g) { g.fillAll (black) ; }
void MainComponent::resized() { blueDial.setBounds (120, 160, 80, 80); greenDial.setBounds (260, 160, 80, 80); yellowDial.setBounds (400, 160, 80, 80); }
|
运行
现在旋钮的基础已经完成,让我们试着运行一下吧!
LookAndFeel
在这个章节中,我们会自定义LookAndFeel类来继续完善这个旋钮。
自定义LookAndFeel
头文件的内容如下,我们根据slider的描述override三个成员函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| #pragma once
#include <JuceHeader.h>
class CustomLookAndFeel : public juce::LookAndFeel_V4 { public: CustomLookAndFeel(); ~CustomLookAndFeel(); juce::Slider::SliderLayout getSliderLayout (juce::Slider& slider) override; void drawRotarySlider (juce::Graphics&, int x, int y, int width, int height, float sliderPosProportional, float rotaryStartAngle, float rotaryEndAngle, juce::Slider&) override; juce::Label* createSliderTextBox (juce::Slider& slider) override; private: juce::Colour blue = juce::Colour::fromFloatRGBA (0.43f, 0.83f, 1.0f, 1.0f); juce::Colour offWhite = juce::Colour::fromFloatRGBA (0.83f, 0.84f, 0.9f, 1.0f); juce::Colour grey = juce::Colour::fromFloatRGBA (0.42f, 0.42f, 0.42f, 1.0f); juce::Colour blackGrey = juce::Colour::fromFloatRGBA (0.2f, 0.2f, 0.2f, 1.0f);
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CustomLookAndFeel); };
|
getSliderLayout()
getSliderLayout() 函数定义了旋钮的位置、大小和中央的文本框。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| #include "CustomLookAndFeel.h"
CustomLookAndFeel::CustomLookAndFeel() {}; CustomLookAndFeel::~CustomLookAndFeel() {};
juce::Slider::SliderLayout CustomLookAndFeel::getSliderLayout (juce::Slider& slider) { auto localBounds = slider.getLocalBounds(); juce::Slider::SliderLayout layout;
layout.textBoxBounds = localBounds; layout.sliderBounds = localBounds;
return layout; }
|
drawRotarySlider()
drawRotarySlider() 函数对这个旋钮的影响很大。他确保了在旋钮被缩放时不会变形,绘制了旋钮背后正方形的四个角,同时让旋钮的角度与数值相关联。
现在让我们override createsSliderTextBox()这个函数,它的功能是使旋钮中央的文本框显示当前旋钮的值。
1 2 3 4 5 6 7 8 9 10 11 12 13
| juce::Label* CustomLookAndFeel::createSliderTextBox (juce::Slider& slider) { auto* l = new juce::Label();
l->setFont (17.0f); l->setJustificationType (juce::Justification::centred); l->setColour (juce::Label::textColourId, slider.findColour (juce::Slider::textBoxTextColourId)); l->setColour (juce::Label::textWhenEditingColourId, slider.findColour (juce::Slider::textBoxTextColourId)); l->setColour (juce::Label::outlineWhenEditingColourId, slider.findColour (juce::Slider::textBoxOutlineColourId)); l->setInterceptsMouseClicks (false, false); return l; }
|
setInterceptMouseClicks()
setInterceptMouseClicks() 是一个非常重要的函数,如果这个函数没有被设定为false,那么你将无法在文本框上拖拽,如果此时文本框在旋钮中央的话,这会是一个致命的问题。
True
False
创建CustomLookAndFeel对象
CustomLookAndFeel将会被应用至旋钮对象,所以在Dial.h中引入这个头文件来准备创建对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| #pragma once
#include <JuceHeader.h> #include "CustomLookAndFeel.h"
class Dial : public juce::Slider { public: ・・・ private: CustomLookAndFeel customLookAndFeel;
juce::Colour grey = juce::Colour::fromFloatRGBA (0.42f, 0.42f, 0.42f, 1.0f); juce::Colour blackGrey = juce::Colour::fromFloatRGBA (0.2f, 0.2f, 0.2f, 1.0f); ・・・
|
为了将LookAndFeel应用至旋钮对象,需要调用setLookAndFeel()函数。同时,设定好文本框的颜色并为接下来的focus mark做好准备(focus mark就是把鼠标移到旋钮上四周会出现的那个矩形框)。
1 2 3 4 5 6 7
| Dial::Dial() { ・・・ setColour (juce::Slider::textBoxTextColourId, blackGrey); setColour (juce::Slider::textBoxOutlineColourId, grey); setLookAndFeel (&customLookAndFeel); }
|
运行
Focus Mark
在这个章节中,我们会设计Focus Mark。
Overriding paint()
override paint() 来让它描述这个标记
1 2 3 4 5 6 7 8 9 10 11 12 13
| class Dial : public juce::Slider { public: Dial(); ~Dial(); void paint (juce::Graphics& g) override; void mouseDown (const juce::MouseEvent& event) override; void mouseUp (const juce::MouseEvent& event) override; private: ・・・
|
定义部分如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| void Dial::paint (juce::Graphics& g) { juce::Slider::paint (g);
if (hasKeyboardFocus (false)) { auto bounds = getLocalBounds().toFloat(); auto h = bounds.getHeight(); auto w = bounds.getWidth(); auto len = juce::jmin (h, w) * 0.07f; auto thick = len / 1.8f; g.setColour (findColour (juce::Slider::textBoxOutlineColourId)); g.drawLine (0.0f, 0.0f, 0.0f, len, thick); g.drawLine (0.0f, 0.0f, len, 0.0f, thick); g.drawLine (0.0f, h, 0.0f, h - len, thick); g.drawLine (0.0f, h, len, h, thick); g.drawLine (w, 0.0f, w, len, thick); g.drawLine (w, 0.0f, w - len, 0.0f, thick); g.drawLine (w, h, w, h - len, thick); g.drawLine (w, h, w - len, h, thick); } }
|
当鼠标被移动到旋钮上时,hasKeyboardFocus() 会返回true。
此外,在focus时下面这个函数必须被调用。
1 2 3 4 5
| Dial::Dial() { ・・・ setWantsKeyboardFocus (true); }
|
现在尝试点击和拖拽每一个旋钮,当鼠标点击到不同的旋钮时,周围的focus mark也会相应变化。
最后,因为此时除非点击其他旋钮,否则focus mark不会消失。为了解决这个问题,setWantsKetboardFocus()必须在下面这个构造函数中被调用。
1 2 3 4 5 6
| MainComponent::MainComponent() { setSize (600, 400); setWantsKeyboardFocus (true); ・・・ }
|
总结
以上就是所有的关于这个旋钮的设计,最终效果如下:
![](164s mark也会同步消失。
在这篇教程中,我们讲解了如何去设计一个modern dial。如果你有任何改进和建议,欢迎评论和交流!感谢您能阅读到这里!